ElasticSearch 聚合查询原理

原理分析

参考:https://www.cnblogs.com/huangying2124/p/12717369.html

https://www.elastic.co/guide/cn/elasticsearch/guide/current/_limiting_memory_usage.html

聚合使用一个叫 doc values 的数据结构(在 Doc Values 介绍 里简单介绍)。 Doc values 可以使聚合更快、更高效并且内存友好,所以理解它的工作方式十分有益。

Doc values 的存在是因为倒排索引只对某些操作是高效的。 倒排索引的优势 在于查找包含某个项的文档,而对于从另外一个方向的相反操作并不高效,即:确定哪些项是否存在单个文档里,聚合需要这种次级的访问模式。

Elasticsearch 中,Doc Values 就是一种列式存储结构。

Doc Values 是在索引时与 倒排索引 同时生成。也就是说 Doc Values倒排索引 一样,基于 Segement 生成并且是不可变的。同时 Doc Values倒排索引 一样序列化到磁盘,这样对性能和扩展性有很大帮助。

因此我们可以通过禁用某些字段的doc_values来节约磁盘空间。

我们对字符串做聚合时,如何时默认的,那么久会出现很坑的点。demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
POST /agg_analysis/data/_bulk
{ "index": {}}
{ "state" : "New York" }
{ "index": {}}
{ "state" : "New Jersey" }

GET /agg_analysis/data/_search
{
"size" : 0,
"aggs" : {
"states" : {
"terms" : {
"field" : "state"
}
}
}
}

{
...
"aggregations": {
"states": {
"buckets": [
{
"key": "new",
"doc_count": 2
},
{
"key": "york",
"doc_count": 1
},
{
"key": "jersey",
"doc_count": 1
},

]
}
}
}

我们对state聚合,结果出现了很多奇怪的桶,这个就是对string这个字段每个词都做了桶,为了禁止这个可以直接加keyword,来处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
GET /data1/data1/_search
{
"size" : 0,
"aggs" : {
"states" : {
"terms" : {
"field" : "state.keyword"
}
}
}
}

"aggregations" : {
"states" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "New York",
"doc_count" : 3
},
{
"key" : "New Jersey",
"doc_count" : 1
},
{
"key" : "New Mexico",
"doc_count" : 1
}
]
}

对于常规字段和text字段的keyword,都是直接走doc values的,这个列索引。效率很高,如果需要对这个字段进行桶聚合,那么就要开启fielddata 。

analyzed字符串的字段,字段分词后占用空间很大,正排索引不能很有效的表示多值字符串,所以正排索引不支持此类字段。

fielddata结构与正排索引类似,是另外一份数据,构建和管理100%在内存中,并常驻于JVM内存堆,极易引起OOM问题。

Fielddata 是 延迟 加载。如果你从来没有聚合一个分析字符串,就不会加载 fielddata 到内存中。此外,fielddata 是基于字段加载的, 这意味着只有很活跃地使用字段才会增加 fielddata 的负担。

实际情况是,fielddata 会加载索引中(针对该特定字段的) 所有的 文档,而不管查询的特异性。逻辑是这样:如果查询会访问文档 X、Y 和 Z,那很有可能会在下一个查询中访问其他文档。因此会把所有的文档全部加载进来。

与 doc values 不同,fielddata 结构不会在索引时创建。相反,它是在查询运行时,动态填充。这可能是一个比较复杂的操作,可能需要一些时间。 将所有的信息一次加载,再将其维持在内存中的方式要比反复只加载一个 fielddata 的部分代价要低。

因此很少使用。

由于fielddata 使用的是jvm堆内存,因此这里扩展下elasticSearch内存分配以及jvm内存。

这里参考另外一篇文档ElasticSearch 内存配置及其原因里面详细描述了elasticSearch的分配策略以及原理。

indices.fielddata.cache.size 控制为 fielddata 分配的堆空间大小。 当你发起一个查询,如果这些字符串之前没有被加载过,分析字符串的聚合将会被加载到 fielddata,加载过,直接取用,前面说过了,这个是全局加载。

默认情况下,设置都是 unbounded ,Elasticsearch 永远都不会从 fielddata 中回收数据。

设想我们正在对日志进行索引,每天使用一个新的索引。通常我们只对过去一两天的数据感兴趣,尽管我们会保留老的索引,但我们很少需要查询它们。不过如果采用默认设置,旧索引的 fielddata 永远不会从缓存中回收! fieldata 会保持增长直到 fielddata 发生断熔(请参阅 断路器),这样我们就无法载入更多的 fielddata。

为了防止发生这样的事情,可以通过在 config/elasticsearch.yml 文件中增加配置为 fielddata 设置一个上限:

1
indices.fielddata.cache.size:  20%

有了这个设置,最久未使用(LRU)的 fielddata 会被回收为新数据腾出空间。